محدود کردن تعداد درخواستها در بازه زمانی برای کال شدن ASP.NET Core Web API
در این مقاله، ما قصد داریم در مورد Rate Limiting در ASP.NET Core صحبت کنیم و راههای پیادهسازی آن را بررسی کنیم.
Rate Limiting چیست؟
APIها منابع و عملکردهای خاصی را در اختیار مشتری قرار می دهند که آن APIها را مصرف می کند. به عنوان مثال، وب سایت یک رستوران یک API از یک سرویس رزرو میز را برای رزرو آنلاین ادغام می کند.
Rate Limiting فرآیند محدود کردن تعداد درخواستها برای یک منبع در یک پنجره زمانی خاص است.
ارائهدهنده خدماتی که یک API برای مصرفکنندگان ارائه میکند، محدودیتهایی در درخواستهای ارائهشده در یک بازه زمانی مشخص خواهد داشت. به عنوان مثال، هر کاربر/ IP , محدودیتی در تعداد درخواست ها به نقطه پایانی API خواهد داشت.
چرا از محدودیت نرخ استفاده می کنیم؟
APIهای عمومی از محدودیت نرخ برای اهداف تجاری برای ایجاد درآمد استفاده می کنند. یک مدل تجاری متداول این است که مبلغ اشتراک مشخصی را برای استفاده از API پرداخت کنید. بنابراین، آنها فقط می توانند بسیاری از تماس های API را قبل از پرداخت بیشتر برای یک طرح ارتقا یافته انجام دهند.
Rate Limiting به محافظت در برابر حملات مخرب ربات کمک می کند. به عنوان مثال، یک هکر می تواند از ربات ها برای برقراری تماس های مکرر با نقطه پایانی API استفاده کند. از این رو، ارائه خدمات برای دیگران در دسترس نیست. این به عنوان حمله انکار سرویس (DoS) شناخته می شود.
محدود کردن نرخ، تنظیم ترافیک به API بر اساس در دسترس بودن زیرساخت. چنین استفاده ای بیشتر مربوط به سرویس های API مبتنی بر ابر است که از استراتژی IaaS «پرداخت در حین حرکت» با ارائه دهندگان ابری استفاده می کنند.
برنامه دمو Web API
بیایید از یک برنامه Web API در حوزه تجارت الکترونیک استفاده کنیم که عملیات ساده CRUD را در لیستی از محصولات انجام می دهد. کنترلر شامل متدهای اقدام مربوطه است:
[HttpGet("")]
[ProducesResponseType(typeof(IEnumerable<Product>), StatusCodes.Status200OK)]
public IActionResult GetAllProducts()
{
return Ok(_repo.GetAll());
}
[HttpGet("{id}")]
[ProducesResponseType(typeof(Product), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetProduct(Guid id)
{
var product = _repo.GetById(id);
return product is not null ? Ok(product) : NotFound();
}
...
برنامه Web API دو نقطه پایانی را نشان میدهد که به ترتیب فهرستی از محصولات و جزئیات تک محصول را دریافت میکنند. بیاید محدودیتی در تعداد درخواستها در یکی از نقاط پایانی اعمال کنیم. به عنوان مثال، نقطه پایانی لیستی از تمام محصولات موجود را به مشتری برمی گرداند. این به احتمال زیاد یک نقطه پایانی محبوب در زمینه ترافیک درخواست خواهد بود.
ProductRepository مسئول تعامل با یک فروشگاه دائمی برای محصولات است:
public class ProductCatalogRepository : IProductCatalogRepository
{
private readonly Dictionary<Guid, Product> _products = new();
private Random _rnd = new Random();
public ProductCatalogRepository()
{
InitializeProductStore();
}
public List<Product> GetAll()
{
return _products.Values.ToList();
}
public Product GetById(Guid id)
{
return _products[id];
}
...
}
برای سادگی، بیایید از in-memory dictionary به عنوان یک دیتابیس استفاده کنیم. البته میدونیم در برنامه واقغی ، از این برای دیتابیس استفاده نمیشه و با یک دیتابیس relational یا non-relational استفاده میشه.
اعمال محدودیت نرخ با استفاده از middleware سفارشی
ASP.NET Core از Rate Limiting پشتیبانی نمی کند. چارچوب ASP.NET Core گزینههای توسعهپذیری میانافزار HTTP را برای این منظور فراهم میکند.
بر اساس نیاز API , ممکن است این امکان را برای تمام endpoint ها یا endpoint خاص اعمال کند. بهترین راه برای رسیدن به این هدف استفاده از دکوراتور است.
دکوراتور
بیایید از یک ویژگی برای تزئین نقطه پایانی که میخواهیم , استفاده کنیم:
[AttributeUsage(AttributeTargets.Method)]
public class LimitRequests : Attribute
{
public int TimeWindow { get; set; }
public int MaxRequests { get; set; }
}
این attribute فقط برای متدها اعمال می شود. دو پراپرتی موجود در attribute حداکثر درخواست های مجاز در یک بازه زمانی خاص را نشان می دهد. رویکرد attribute به ما انعطافپذیری میدهد تا پیکربندیهای محدودکننده نرخ متفاوت را برای endpoint های مختلف در یک API یکسان اعمال کنیم.
اجازه دهید دکوراتور LimitRequests را در نقطه پایانی /products اعمال کنیم و آن را طوری پیکربندی کنیم که حداکثر 2 درخواست برای یک بازه 5 ثانیه ای مجاز باشد:
[HttpGet("")]
[ProducesResponseType(typeof(IEnumerable<Product>), StatusCodes.Status200OK)]
[LimitRequests(MaxRequests = 2, TimeWindow = 5)]
public IActionResult GetAllProducts()
{
return Ok(_repo.GetAll());
}
اکنون درخواست سوم در پنجره 5 ثانیه ای پاسخ موفقیت آمیزی را بر نمی گرداند.
Middleware
میانافزار سفارشی RateLimitingMiddleware شامل منطق محدود کردن نرخ است:
public async Task InvokeAsync(HttpContext context)
{
var endpoint = context.GetEndpoint();
var decorator = endpoint?.Metadata.GetMetadata<LimitRequests>();
if (decorator is null)
{
await _next(context);
return;
}
var key = GenerateClientKey(context);
var clientStatistics = await GetClientStatisticsByKey(key);
if (clientStatistics != null &&
DateTime.UtcNow < clientStatistics.LastSuccessfulResponseTime.AddSeconds(decorator.TimeWindow) &&
clientStatistics.NumberOfRequestsCompletedSuccessfully == rateLimitingDecorator.MaxRequests)
{
context.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
return;
}
await UpdateClientStatisticsStorage(key, rateLimitingDecorator.MaxRequests);
await _next(context);
}
ابتدا، بیایید بررسی کنیم که آیا نقطه پایانی درخواستی حاوی دکوراتور LimitRequests است یا خیر. بنابراین، اگر دکوراتور وجود نداشته باشد، درخواست به میان افزار بعدی در خط لوله منتقل می شود.
اگر دکوراتور در نقطه پایانی وجود دارد، بیایید یک کلید منحصر به فرد ایجاد کنیم. این کلید ترکیبی از مسیر نقطه پایانی و آدرس IP مشتری است:
private static string GenerateClientKey(HttpContext context)
=> $"{context.Request.Path}_{context.Connection.RemoteIpAddress}";
میتوانیم درخواستها را در یک بازه زمانی مشخص بر اساس آدرس IP، شناسه کاربر یا کلید مشتری ,محدود کنیم. در اینجا، ما آدرس های IP را به عنوان شناسه مشتری انتخاب کرده ایم. بنابراین، ما انعطاف پذیری برای انتخاب استراتژی برای شناسه محدودیت نرخ داریم.
حال، بیایید از این کلید برای دریافت نمونه ای از کلاس ClientStatistics از یک کش توزیع شده استفاده کنیم:
private async Task<ClientStatistics> GetClientStatisticsByKey(string key)
{
return await _cache.GetCacheValueAsync<ClientStatistics>(key);
}
public class ClientStatistics
{
public DateTime LastSuccessfulResponseTime { get; set; }
public int NumberOfRequestsCompletedSuccessfully { get; set; }
}
نمونه ClientStatistics رکوردی است از تعداد دفعاتی که به مشتری خاص پاسخ داده شده و زمان آخرین پاسخ موفق. در اینجا، میان افزار درخواست فعلی را بر اساس این داده ها کاهش می دهد.
برای یک API متعادل بار، در حالت ایدهآل، دادههای آمار کلاینت را در یک کش توزیع شده مانند Redis یا Memcached ذخیره میکنیم. با این حال، برای سادگی، اجازه دهید از یک کش در حافظه در اینجا استفاده کنیم:
builder.Services.AddDistributedMemoryCache();
در نهایت، بیایید از آمار مشتری برای بررسی اینکه آیا درخواست فعلی از حداکثر محدودیت درخواست در پنجره زمانی برای نقطه پایانی عبور کرده است استفاده کنیم. در چنین سناریویی، کلاینت کد وضعیت 429 را دریافت می کند. سپس، کد حافظه پنهان را با آمار مشتری فعلی برای درخواست موفقیت آمیز به روز می کند.
اگر تعداد درخواست ها محدودیت درخواست نقطه پایانی را نقض نکند، مشتری لیستی از محصولات با کد وضعیت 200 دریافت می کند: